﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CSharpExperiments.Nullsafety
{
	public static class Maybe
	{
		public static Maybe<A> From<A>(A obj)
		{
			if (! IsMaybeable(typeof(A)) || ! Object.Equals(obj, default(A)))
				return new Maybe<A>(obj, true);
			else
				return None<A>();
		}

		public static Maybe<A> None<A>()
		{
			return new Maybe<A>(default(A), false);
		}

		public static Maybe<A> AsMaybe<A>(this A obj)
		{
			return From<A>(obj);
		}
		
		public static bool IsMaybeable(Type type)
		{
			return ! (type.IsEnum || (type.IsPrimitive && (type != typeof(Char)) && (type != typeof(Boolean))));
		}

		public static Nullable<V> AsNullable<V>(this Maybe<V> maybe) where V : struct
		{
			return new Nullable<V>(maybe.Value);
		}

		public static Maybe<A> Flatten<A>(this Maybe<Maybe<A>> nested)
		{
			return nested.HasValue ? nested.Value : None<A>();
		}

		public static Maybe<B> Apply<A, B>(this Maybe<A> x, Maybe<A> y, Func<A, A, B> func)
		{
			return (! x.HasValue || ! y.HasValue) ? None<B>() : From(func(x.Value, y.Value));
		}

		public static Maybe<A> ApplyFlat<A>(this Maybe<A> x, Maybe<A> y, Func<A, A, Maybe<A>> func)
		{
			return Apply(x, y, func).Flatten();
		}

		public static Maybe<int> ToInt32(String str)
		{
			int i;
			if (Int32.TryParse(str, out i))
				return Maybe.From(i);
			return Maybe.None<int>();
		}
		
		public static Maybe<double> ToDouble(String str)
		{
			double d;
			if (Double.TryParse(str, out d))
				return Maybe.From(d);
			return Maybe.None<double>();
		}

		public static Maybe<Int32> Sum(this Maybe<Int32> x, Maybe<Int32> y) { return x.Apply(y, (i, j) => i + j); }
		public static Maybe<Int64> Sum(this Maybe<Int64> x, Maybe<Int64> y) { return x.Apply(y, (i, j) => i + j); }
		public static Maybe<Single> Sum(this Maybe<Single> x, Maybe<Single> y) { return x.Apply(y, (i, j) => i + j); }
		public static Maybe<Double> Sum(this Maybe<Double> x, Maybe<Double> y) { return x.Apply(y, (i, j) => i + j); }
		public static Maybe<Int32> Min(this Maybe<Int32> x, Maybe<Int32> y) { return x.Apply(y, Math.Min); }
		public static Maybe<Int64> Min(this Maybe<Int64> x, Maybe<Int64> y) { return x.Apply(y, Math.Min); }
		public static Maybe<Single> Min(this Maybe<Single> x, Maybe<Single> y) { return x.Apply(y, Math.Min); }
		public static Maybe<Double> Min(this Maybe<Double> x, Maybe<Double> y) { return x.Apply(y, Math.Min); }
		public static Maybe<Int32> Max(this Maybe<Int32> x, Maybe<Int32> y) { return x.Apply(y, Math.Max); }
		public static Maybe<Int64> Max(this Maybe<Int64> x, Maybe<Int64> y) { return x.Apply(y, Math.Max); }
		public static Maybe<Single> Max(this Maybe<Single> x, Maybe<Single> y) { return x.Apply(y, Math.Max); }
		public static Maybe<Double> Max(this Maybe<Double> x, Maybe<Double> y) { return x.Apply(y, Math.Max); }
	}

	public struct Maybe<A>
	{
		public A Value { get; private set; }
		public bool HasValue { get; private set; }

		internal Maybe(A value, bool isSome) : this()
		{
			Value = value;
			HasValue = isSome;
		}

		[Obsolete("Unsafe - may throw NullReferenceException - use GetOrDefault, ForEach, If, etc.")]
		public A Get()
		{
			if (! HasValue) throw new ApplicationException("Maybe has no value");
			return Value;
		}
		
		[Obsolete("Unsafe implicit conversion - may throw NullReferenceException - use GetOrDefault, ForEach, If, etc.")]
		public static implicit operator A(Maybe<A> maybe)
		{
			// Ignore warning
			#pragma warning disable 0612, 0618
			return maybe.Get();
			#pragma warning restore 0612, 0618
		}

		public static implicit operator Maybe<A>(A value)
		{
			return Maybe.From(value);
		}

		public Type WrappedType { get { return typeof(A); } }

		public Maybe<B> Select<B>(Func<A, B> func)
		{
			return HasValue ? Maybe.From(func(Value)) : Maybe.None<B>();
		}

		public Maybe<B> SelectFlat<B>(Func<A, Maybe<B>> func)
		{
			return HasValue ? func(Value) : Maybe.None<B>();
		}

		public Maybe<A> Where(Func<A, bool> filter)
		{
			return (! HasValue || filter(Value)) ? this : Maybe.None<A>();
		}

		public void ForEach(Action<A> action)
		{
			if (HasValue) action(Value);
		}

		public void If(Action<A> ifDefined, Action ifNot)
		{
			if (HasValue) ifDefined(Value);
			else          ifNot();
		}

		public T If<T>(Func<A, T> ifDefined, Func<T> ifNot)
		{
			if (HasValue) return ifDefined(Value);
			else          return ifNot();
		}

		public Maybe<B> Cast<B>()
		{
			return HasValue ? Maybe.From<B>((B) Convert.ChangeType(Value, typeof(B))) : Maybe.None<B>();
		}

		public Maybe<B> OfType<B>()
		{
			return (HasValue && (Value is B)) ? Cast<B>() : Maybe.None<B>();
		}

		public A GetOrDefault(A defaultValue)
		{
			return HasValue ? Value : defaultValue;
		}

		public int Count()
		{
			return HasValue ? 1 : 0;
		}

		public IEnumerable<A> AsEnumerable()
		{
			if (HasValue)
				yield return Value;
			else
				yield break;
		}

		public List<A> ToList()
		{
			return HasValue ? new List<A>() { Value } : new List<A>();
		}

		public A[] ToArray()
		{
			return HasValue ? new A[] { Value } : new A[0];
		}

		public override String ToString()
		{
			return HasValue ? "Some(" + Value.ToString() + ")" : "None";
		}

		public override int GetHashCode()
		{
			return HasValue ? (Value.GetHashCode() ^ 0x7e7e7e7e) : 1;
		}

		public override bool Equals(Object other)
		{
			if (! (other is Maybe<A>)) return false;
			Maybe<A> that = (Maybe<A>) other;
			return this.HasValue ? (that.HasValue && Object.Equals(this.Value, that.Value)) : (! that.HasValue);
		}
	}
}
